package info.niwota.dnd;

import android.app.Service;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.SystemClock;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;

/**
 * A ViewGroup that coordinated dragging across its descendants
 */
public class DragLayer extends FrameLayout implements DragController {
	private static final int SCROLL_DELAY = 600;
	private static final int SCROLL_ZONE = 20;
	private static final int VIBRATE_DURATION = 35;
	private static final int ANIMATION_SCALE_UP_DURATION = 110;

	// Number of pixels to add to the dragged item for scaling
	private static final float DRAG_SCALE = 48.0f; // 24

	private boolean mDragging = false;
	private boolean mShouldDrop;
	private float mLastMotionX;
	private float mLastMotionY;

	/**
	 * The bitmap that is currently being dragged
	 */
	private Bitmap mDragBitmap = null;
	private View mOriginator;

	private int mBitmapOffsetX;
	private int mBitmapOffsetY;

	/**
	 * X offset from where we touched on the cell to its upper-left corner
	 */
	private float mTouchOffsetX;

	/**
	 * Y offset from where we touched on the cell to its upper-left corner
	 */
	private float mTouchOffsetY;

	/**
	 * Utility rectangle
	 */
	private Rect mDragRect = new Rect();

	/**
	 * Where the drag originated
	 */
	private DragSource mDragSource;

	/**
	 * The data associated with the object being dragged
	 */
	private Object mDragInfo;

	private final Rect mRect = new Rect();
	private final int[] mDropCoordinates = new int[2];

	private Vibrator mVibrator;

	private DragListener mListener;

	private DragScroller mDragScroller;

	private static final int SCROLL_OUTSIDE_ZONE = 0;
	private static final int SCROLL_WAITING_IN_ZONE = 1;

	private static final int SCROLL_LEFT = 0;
	private static final int SCROLL_RIGHT = 1;

	private int mScrollState = SCROLL_OUTSIDE_ZONE;

	private ScrollRunnable mScrollRunnable = new ScrollRunnable();
	private View mIgnoredDropTarget;

	private RectF mDragRegion;
	private boolean mEnteredRegion;
	private DropTarget mLastDropTarget;

	private final Paint mTrashPaint = new Paint();
	private final Paint mEstimatedPaint = new Paint();
	private Paint mDragPaint;

	/**
	 * If true, draw a "snag" showing where the object currently being dragged
	 * would end up if dropped from current location.
	 */
	private static final boolean DRAW_TARGET_SNAG = true; //false

	private Rect mEstimatedRect = new Rect();
	private float[] mDragCenter = new float[2];
	private float[] mEstimatedCenter = new float[2];

	private boolean mDrawEstimated = false;

	private int mTriggerWidth = -1;
	private int mTriggerHeight = -1;

	//private static final int DISTANCE_DRAW_SNAG = 20;

	private static final int ANIMATION_STATE_STARTING = 1;
	private static final int ANIMATION_STATE_RUNNING = 2;
	private static final int ANIMATION_STATE_DONE = 3;

	private static final int ANIMATION_TYPE_SCALE = 1;

	private float mAnimationFrom;
	private float mAnimationTo;
	private int mAnimationDuration;
	private long mAnimationStartTime;
	private int mAnimationType;
	private int mAnimationState = ANIMATION_STATE_DONE;

	private InputMethodManager mInputMethodManager;

	/**
	 * Used to create a new DragLayer from XML.
	 * 
	 * @param context
	 *            The application's context.
	 * @param attrs
	 *            The attribtues set containing the DragView's customization
	 *            values.
	 */
	public DragLayer(Context context, AttributeSet attrs) {
		super(context, attrs);
		
		try {
			mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		//final int srcColor = context.getResources().getColor(
		//		R.color.delete_color_filter);
		//mTrashPaint.setColorFilter(new PorterDuffColorFilter(srcColor,
		//		PorterDuff.Mode.SRC_ATOP));

		// Make estimated paint area in gray
		//int snagColor = context.getResources().getColor(
		//		R.color.snag_callout_color);
		//mEstimatedPaint.setColor(snagColor);
		//mEstimatedPaint.setStrokeWidth(3);
		//mEstimatedPaint.setAntiAlias(true);
	}

	public void startDrag(View v, DragSource source, Object dragInfo,
			int dragAction) {
		// Hide soft keyboard, if visible
		if (mInputMethodManager == null) {
			mInputMethodManager = (InputMethodManager) getContext()
					.getSystemService(Context.INPUT_METHOD_SERVICE);
		}
		mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);

		if (mListener != null) {
			mListener.onDragStart(v, source, dragInfo, dragAction);
		}

		Rect r = mDragRect;
		r.set(v.getScrollX(), v.getScrollY(), 0, 0);

		offsetDescendantRectToMyCoords(v, r);
		mTouchOffsetX = mLastMotionX - r.left;
		mTouchOffsetY = mLastMotionY - r.top;

		v.clearFocus();
		v.setPressed(false);

		boolean willNotCache = v.willNotCacheDrawing();
		v.setWillNotCacheDrawing(false);

		// Reset the drawing cache background color to fully transparent
		// for the duration of this operation
		int color = v.getDrawingCacheBackgroundColor();
		v.setDrawingCacheBackgroundColor(0);

		if (color != 0) {
			v.destroyDrawingCache();
		}
		v.buildDrawingCache();
		Bitmap viewBitmap = v.getDrawingCache();
		int width = viewBitmap.getWidth();
		int height = viewBitmap.getHeight();

		mTriggerWidth = width * 2 / 3;
		mTriggerHeight = height * 2 / 3;

		Matrix scale = new Matrix();
		float scaleFactor = v.getWidth();
		scaleFactor = (scaleFactor + DRAG_SCALE) / scaleFactor;
		scale.setScale(scaleFactor, scaleFactor);

		mAnimationTo = 1.0f;
		mAnimationFrom = 1.0f / scaleFactor;
		mAnimationDuration = ANIMATION_SCALE_UP_DURATION;
		mAnimationState = ANIMATION_STATE_STARTING;
		mAnimationType = ANIMATION_TYPE_SCALE;

		mDragBitmap = Bitmap.createBitmap(viewBitmap, 0, 0, width, height,
				scale, true);
		v.destroyDrawingCache();
		v.setWillNotCacheDrawing(willNotCache);
		v.setDrawingCacheBackgroundColor(color);

		final Bitmap dragBitmap = mDragBitmap;
		mBitmapOffsetX = (dragBitmap.getWidth() - width) / 2;
		mBitmapOffsetY = (dragBitmap.getHeight() - height) / 2;

		if (dragAction == DRAG_ACTION_MOVE) {
			v.setVisibility(GONE);
		}

		mDragPaint = null;
		mDragging = true;
		mShouldDrop = true;
		mOriginator = v;
		mDragSource = source;
		mDragInfo = dragInfo;

		if (mVibrator != null) {
			mVibrator.vibrate(VIBRATE_DURATION);
		}
		mEnteredRegion = false;

		invalidate();
	}

	@Override
	public boolean dispatchKeyEvent(KeyEvent event) {
		return mDragging || super.dispatchKeyEvent(event);
	}

	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);

		float mScrollX = getScrollX();
		float mScrollY = getScrollY();

		if (mDragging && mDragBitmap != null) {
			if (mAnimationState == ANIMATION_STATE_STARTING) {
				mAnimationStartTime = SystemClock.uptimeMillis();
				mAnimationState = ANIMATION_STATE_RUNNING;
			}

			if (mAnimationState == ANIMATION_STATE_RUNNING) {
				float normalized = (float) (SystemClock.uptimeMillis() - mAnimationStartTime)
						/ mAnimationDuration;
				if (normalized >= 1.0f) {
					mAnimationState = ANIMATION_STATE_DONE;
				}
				normalized = Math.min(normalized, 1.0f);
				final float value = mAnimationFrom
						+ (mAnimationTo - mAnimationFrom) * normalized;

				switch (mAnimationType) {
				case ANIMATION_TYPE_SCALE:
					final Bitmap dragBitmap = mDragBitmap;
					canvas.save();
					canvas.translate(mScrollX + mLastMotionX - mTouchOffsetX
							- mBitmapOffsetX, mScrollY + mLastMotionY
							- mTouchOffsetY - mBitmapOffsetY);
					canvas.translate(
							(dragBitmap.getWidth() * (1.0f - value)) / 2,
							(dragBitmap.getHeight() * (1.0f - value)) / 2);
					canvas.scale(value, value);
					
					canvas.drawBitmap(dragBitmap, 0.0f, 0.0f, mDragPaint);
					
					canvas.restore();
					break;
				}
			} else {
				// Only draw estimate drop "snag" when requested
				if (DRAW_TARGET_SNAG && mDrawEstimated) {
					canvas.drawLine(mDragCenter[0], mDragCenter[1],
							mEstimatedCenter[0], mEstimatedCenter[1],
							mEstimatedPaint);
					canvas.drawCircle(mEstimatedCenter[0], mEstimatedCenter[1],
							8, mEstimatedPaint);
				}

				// Draw actual icon being dragged
				canvas.drawBitmap(mDragBitmap, mScrollX + mLastMotionX
						- mTouchOffsetX - mBitmapOffsetX, mScrollY
						+ mLastMotionY - mTouchOffsetY - mBitmapOffsetY,
						mDragPaint);
			}
		}
	}

	private void endDrag() {
		if (mDragging) {
			mDragging = false;
			if (mDragBitmap != null) {
				mDragBitmap.recycle();
			}
			if (mOriginator != null) {
				mOriginator.setVisibility(VISIBLE);
			}
			if (mListener != null) {
				mListener.onDragEnd();
			}
		}
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		final int action = ev.getAction();

		final float x = ev.getX();
		final float y = ev.getY();

		switch (action) {
		case MotionEvent.ACTION_MOVE:
			break;

		case MotionEvent.ACTION_DOWN:
			// Remember location of down touch
			mLastMotionX = x;
			mLastMotionY = y;
			mLastDropTarget = null;
			break;

		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			if (mShouldDrop && drop(x, y)) {
				mShouldDrop = false;
			}
			endDrag();
			break;
		}

		return mDragging;
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (!mDragging) {
			return false;
		}

		final int action = ev.getAction();
		final float x = ev.getX();
		final float y = ev.getY();
		//
		switch (action) {
		case MotionEvent.ACTION_DOWN:

			// Remember where the motion event started
			mLastMotionX = x;
			mLastMotionY = y;

			if ((x < SCROLL_ZONE) || (x > getWidth() - SCROLL_ZONE)) {
				mScrollState = SCROLL_WAITING_IN_ZONE;
				postDelayed(mScrollRunnable, SCROLL_DELAY);
			} else {
				mScrollState = SCROLL_OUTSIDE_ZONE;
			}

			break;
		case MotionEvent.ACTION_MOVE:
			final int scrollX = getScrollX();
			final int scrollY = getScrollY();

			final float touchX = mTouchOffsetX;
			final float touchY = mTouchOffsetY;

			final int offsetX = mBitmapOffsetX;
			final int offsetY = mBitmapOffsetY;

			int left = (int) (scrollX + mLastMotionX - touchX - offsetX);
			int top = (int) (scrollY + mLastMotionY - touchY - offsetY);

			//
			final Bitmap dragBitmap = mDragBitmap;
			final int width = dragBitmap.getWidth();
			final int height = dragBitmap.getHeight();

			final Rect rect = mRect;
			rect.set(left - 1, top - 1, left + width + 1, top + height + 1);

			mLastMotionX = x;
			mLastMotionY = y;

			left = (int) (scrollX + x - touchX - offsetX);
			top = (int) (scrollY + y - touchY - offsetY);
			
			// Invalidate current icon position
			rect.union(left - 1, top - 1, left + width + 1, top + height + 1);

			mDragCenter[0] = rect.centerX();
			mDragCenter[1] = rect.centerY();

			// Invalidate any old estimated location
			if (DRAW_TARGET_SNAG && mDrawEstimated) {
				rect.union(mEstimatedRect);
			}

			final int[] coordinates = mDropCoordinates;
			DropTarget dropTarget = findDropTarget((int) x, (int) y,
					coordinates);
			if (dropTarget != null) {
				if (mLastDropTarget == dropTarget) {
					dropTarget.onDragOver(mDragSource, coordinates[0],
							coordinates[1], (int) mTouchOffsetX,
							(int) mTouchOffsetY, mDragInfo);
				} else {
					if (mLastDropTarget != null) {
						mLastDropTarget.onDragExit(mDragSource, coordinates[0],
								coordinates[1], (int) mTouchOffsetX,
								(int) mTouchOffsetY, mDragInfo);
					}
					dropTarget.onDragEnter(mDragSource, coordinates[0],
							coordinates[1], (int) mTouchOffsetX,
							(int) mTouchOffsetY, mDragInfo);
				}
			} else {
				if (mLastDropTarget != null) {
					mLastDropTarget.onDragExit(mDragSource, coordinates[0],
							coordinates[1], (int) mTouchOffsetX,
							(int) mTouchOffsetY, mDragInfo);
				}
			}

			// Render estimated drop "snag" only outside of width
			mDrawEstimated = false;
			if (DRAW_TARGET_SNAG && dropTarget != null) {
				Rect foundEstimate = dropTarget.estimateDropLocation(
						mDragSource, (int) (scrollX + mLastMotionX),
						(int) (scrollY + mLastMotionY), (int) mTouchOffsetX,
						(int) mTouchOffsetY, mDragInfo, mEstimatedRect);

				if (foundEstimate != null) {
					mEstimatedCenter[0] = foundEstimate.centerX();
					mEstimatedCenter[1] = foundEstimate.centerY();

					int deltaX = (int) Math.abs(mEstimatedCenter[0]
							- mDragCenter[0]);
					int deltaY = (int) Math.abs(mEstimatedCenter[1]
							- mDragCenter[1]);

					if (deltaX > mTriggerWidth || deltaY > mTriggerHeight) {
						mDrawEstimated = true;
					}
				}
			}

			// Include new estimated area in invalidated rectangle
			if (DRAW_TARGET_SNAG && mDrawEstimated) {
				rect.union(mEstimatedRect);
			}
			invalidate(rect);

			mLastDropTarget = dropTarget;

			boolean inDragRegion = false;
			if (mDragRegion != null) {
				final RectF region = mDragRegion;
				final boolean inRegion = region.contains(ev.getRawX(), ev
						.getRawY());
				if (!mEnteredRegion && inRegion) {
					mDragPaint = mTrashPaint;
					mEnteredRegion = true;
					inDragRegion = true;
				} else if (mEnteredRegion && !inRegion) {
					mDragPaint = null;
					mEnteredRegion = false;
				}
			}

			if (!inDragRegion && x < SCROLL_ZONE) {
				if (mScrollState == SCROLL_OUTSIDE_ZONE) {
					mScrollState = SCROLL_WAITING_IN_ZONE;
					mScrollRunnable.setDirection(SCROLL_LEFT);
					postDelayed(mScrollRunnable, SCROLL_DELAY);
				}
			} else if (!inDragRegion && x > getWidth() - SCROLL_ZONE) {
				if (mScrollState == SCROLL_OUTSIDE_ZONE) {
					mScrollState = SCROLL_WAITING_IN_ZONE;
					mScrollRunnable.setDirection(SCROLL_RIGHT);
					postDelayed(mScrollRunnable, SCROLL_DELAY);
				}
			} else {
				if (mScrollState == SCROLL_WAITING_IN_ZONE) {
					mScrollState = SCROLL_OUTSIDE_ZONE;
					mScrollRunnable.setDirection(SCROLL_RIGHT);
					removeCallbacks(mScrollRunnable);
				}
			}

			break;
		case MotionEvent.ACTION_UP:
			removeCallbacks(mScrollRunnable);
			if (mShouldDrop) {
				drop(x, y);
				mShouldDrop = false;
			}
			endDrag();

			break;
		case MotionEvent.ACTION_CANCEL:
			endDrag();
		}

		return true;
	}

	private boolean drop(float x, float y) {
		invalidate();

		final int[] coordinates = mDropCoordinates;
		DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);

		//
		float dx = x - mTouchOffsetX;
		float dy = y - mTouchOffsetY;
		dx = Math.max(0, Math.min(dx, getWidth()));
		dy = Math.max(0, Math.min(dy, getHeight()));
		//
		if (dropTarget != null) {
			dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
					(int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo);
			if (dropTarget.acceptDrop(mDragSource, coordinates[0],
					coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY,
					mDragInfo)) {
				dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1],
						(int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo);
				mDragSource.onDropCompleted((View) dropTarget, (int) dx,
						(int) dy, true);
				return true;
			} else {
				mDragSource.onDropCompleted((View) dropTarget, (int) dx,
						(int) dy, false);
				return true;
			}
		}
		// source to handle drop
		if (mDragging) {
			mDragSource.onDropCompleted(this, (int) dx, (int) dy, true);
		}
		return false;
	}

	DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
		return findDropTarget(this, x, y, dropCoordinates);
	}

	private DropTarget findDropTarget(ViewGroup container, int x, int y,
			int[] dropCoordinates) {
		final Rect r = mDragRect;
		final int count = container.getChildCount();
		final int scrolledX = x + container.getScrollX();
		final int scrolledY = y + container.getScrollY();
		final View ignoredDropTarget = mIgnoredDropTarget;

		for (int i = count - 1; i >= 0; i--) {
			final View child = container.getChildAt(i);
			if (child.getVisibility() == VISIBLE && child != ignoredDropTarget) {
				child.getHitRect(r);
				if (r.contains(scrolledX, scrolledY)) {
					DropTarget target = null;
					if (child instanceof ViewGroup) {
						x = scrolledX - child.getLeft();
						y = scrolledY - child.getTop();
						target = findDropTarget((ViewGroup) child, x, y,
								dropCoordinates);
					}
					if (target == null) {
						if (child instanceof DropTarget) {
							// Only consider this child if they will accept
							DropTarget childTarget = (DropTarget) child;
							if (childTarget.acceptDrop(mDragSource, x, y, 0, 0,
									mDragInfo)) {
								dropCoordinates[0] = x;
								dropCoordinates[1] = y;
								return (DropTarget) child;
							} else {
								return null;
							}
						}
					} else {
						return target;
					}
				}
			}
		}

		return null;
	}

	public void setDragScoller(DragScroller scroller) {
		mDragScroller = scroller;
	}

	public void setDragListener(DragListener l) {
		mListener = l;
	}

	public void removeDragListener(DragListener l) {
		mListener = null;
	}

	/**
	 * Specifies the view that must be ignored when looking for a drop target.
	 * 
	 * @param view
	 *            The view that will not be taken into account while looking for
	 *            a drop target.
	 */
	void setIgnoredDropTarget(View view) {
		mIgnoredDropTarget = view;
	}

	/**
	 * Specifies the delete region.
	 * 
	 * @param region
	 *            The rectangle in screen coordinates of the delete region.
	 */
	void setDeleteRegion(RectF region) {
		mDragRegion = region;
	}

	private class ScrollRunnable implements Runnable {
		private int mDirection;

		ScrollRunnable() {
		}

		public void run() {
			if (mDragScroller != null) {
				mDrawEstimated = false;
				if (mDirection == SCROLL_LEFT) {
					mDragScroller.scrollLeft();
				} else {
					mDragScroller.scrollRight();
				}
				mScrollState = SCROLL_OUTSIDE_ZONE;
			}
		}

		void setDirection(int direction) {
			mDirection = direction;
		}
	}
}
